<!doctype html>
<html>
<head>
<title>Learning JebGL &mdash; lesson 9</title>
<meta charset="utf-8">

<script type="text/javascript" src="glMatrix-0.9.5.min.js"></script>
<script type="text/javascript" src="jebgl.js"></script>

<script id="shader-fs" type="x-shader/x-fragment">
    #ifdef GL_ES
    precision highp float;
    #endif

    varying vec2 vTextureCoord;

    uniform sampler2D uSampler;

    uniform vec3 uColor;

    void main(void) {
        vec4 textureColor = texture2D(uSampler, vec2(vTextureCoord.s, vTextureCoord.t));
        gl_FragColor = textureColor * vec4(uColor, 1.0);
    }
</script>

<script id="shader-vs" type="x-shader/x-vertex">
    attribute vec3 aVertexPosition;
    attribute vec2 aTextureCoord;

    uniform mat4 uMVMatrix;
    uniform mat4 uPMatrix;

    varying vec2 vTextureCoord;

    void main(void) {
        gl_Position = uPMatrix * uMVMatrix * vec4(aVertexPosition, 1.0);
        vTextureCoord = aTextureCoord;
    }
</script>


<script type="text/javascript">

    var gl;

    function initGL(canvas) {
        try {
            gl = canvas.getContext("experimental-webgl");
            gl.viewportWidth = canvas.width;
            gl.viewportHeight = canvas.height;
        } catch (e) {
        }
        if (!gl) {
            alert("Could not initialise WebGL, sorry :-(");
        }
    }


    function getShader(gl, id) {
        var shaderScript = document.getElementById(id);
        if (!shaderScript) {
            return null;
        }

        var str = "";
        str += shaderScript.innerHTML;

        var shader;
        if (shaderScript.type == "x-shader/x-fragment") {
            shader = gl.createShader(gl.FRAGMENT_SHADER);
        } else if (shaderScript.type == "x-shader/x-vertex") {
            shader = gl.createShader(gl.VERTEX_SHADER);
        } else {
            return null;
        }

        gl.shaderSource(shader, str);
        gl.compileShader(shader);

        if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) {
            alert(gl.getShaderInfoLog(shader));
            return null;
        }

        return shader;
    }


    var shaderProgram;

    function initShaders() {
        var fragmentShader = getShader(gl, "shader-fs");
        var vertexShader = getShader(gl, "shader-vs");

        shaderProgram = gl.createProgram();
        gl.attachShader(shaderProgram, vertexShader);
        gl.attachShader(shaderProgram, fragmentShader);
        gl.linkProgram(shaderProgram);

        if (!gl.getProgramParameter(shaderProgram, gl.LINK_STATUS)) {
            alert("Could not initialise shaders");
        }

        gl.useProgram(shaderProgram);

        shaderProgram.vertexPositionAttribute = gl.getAttribLocation(shaderProgram, "aVertexPosition");
        gl.enableVertexAttribArray(shaderProgram.vertexPositionAttribute);

        shaderProgram.textureCoordAttribute = gl.getAttribLocation(shaderProgram, "aTextureCoord");
        gl.enableVertexAttribArray(shaderProgram.textureCoordAttribute);

        shaderProgram.pMatrixUniform = gl.getUniformLocation(shaderProgram, "uPMatrix");
        shaderProgram.mvMatrixUniform = gl.getUniformLocation(shaderProgram, "uMVMatrix");
        shaderProgram.samplerUniform = gl.getUniformLocation(shaderProgram, "uSampler");
        shaderProgram.colorUniform = gl.getUniformLocation(shaderProgram, "uColor");
    }


    function handleLoadedTexture(texture) {
        gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL, true);
        gl.bindTexture(gl.TEXTURE_2D, texture);
        gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, texture.image);
        gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR);
        gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);

        gl.bindTexture(gl.TEXTURE_2D, null);
    }


    var starTexture;

    function initTexture() {
        starTexture = gl.createTexture();
        starTexture.image = document.getElementById('star');
        handleLoadedTexture(starTexture);
    }


    var mvMatrix = mat4.create();
    var mvMatrixStack = [];
    var pMatrix = mat4.create();

    function mvPushMatrix() {
        var copy = mat4.create();
        mat4.set(mvMatrix, copy);
        mvMatrixStack.push(copy);
    }

    function mvPopMatrix() {
        if (mvMatrixStack.length == 0) {
            throw "Invalid popMatrix!";
        }
        mvMatrix = mvMatrixStack.pop();
    }


    function setMatrixUniforms() {
        gl.uniformMatrix4fv(shaderProgram.pMatrixUniform, false, pMatrix);
        gl.uniformMatrix4fv(shaderProgram.mvMatrixUniform, false, mvMatrix);
    }


    function degToRad(degrees) {
        return degrees * Math.PI / 180;
    }


    var currentlyPressedKeys = {};

    function handleKeyDown(event) {
	event = event || window.event;
        currentlyPressedKeys[event.keyCode] = true;
    }


    function handleKeyUp(event) {
	event = event || window.event;
        currentlyPressedKeys[event.keyCode] = false;
    }


    var zoom = -15;


    var tilt = 90;
    var spin = 0;


    function handleKeys() {
        if (currentlyPressedKeys[33]) {
            // Page Up
            zoom -= 0.1;
        }
        if (currentlyPressedKeys[34]) {
            // Page Down
            zoom += 0.1;
        }
        if (currentlyPressedKeys[38]) {
            // Up cursor key
            tilt += 2;
        }
        if (currentlyPressedKeys[40]) {
            // Down cursor key
            tilt -= 2;
        }
    }


    var starVertexPositionBuffer;
    var starVertexTextureCoordBuffer;

    function initBuffers() {
        starVertexPositionBuffer = gl.createBuffer();
        gl.bindBuffer(gl.ARRAY_BUFFER, starVertexPositionBuffer);
        vertices = [
            -1.0, -1.0,  0.0,
             1.0, -1.0,  0.0,
            -1.0,  1.0,  0.0,
             1.0,  1.0,  0.0
        ];
        gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(vertices), gl.STATIC_DRAW);
        starVertexPositionBuffer.itemSize = 3;
        starVertexPositionBuffer.numItems = 4;

        starVertexTextureCoordBuffer = gl.createBuffer();
        gl.bindBuffer(gl.ARRAY_BUFFER, starVertexTextureCoordBuffer);
        var textureCoords = [
            0.0, 0.0,
            1.0, 0.0,
            0.0, 1.0,
            1.0, 1.0
        ];
        gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(textureCoords), gl.STATIC_DRAW);
        starVertexTextureCoordBuffer.itemSize = 2;
        starVertexTextureCoordBuffer.numItems = 4;
    }


    function drawStar() {
        gl.activeTexture(gl.TEXTURE0);
        gl.bindTexture(gl.TEXTURE_2D, starTexture);
        gl.uniform1i(shaderProgram.samplerUniform, 0);

        gl.bindBuffer(gl.ARRAY_BUFFER, starVertexTextureCoordBuffer);
        gl.vertexAttribPointer(shaderProgram.textureCoordAttribute, starVertexTextureCoordBuffer.itemSize, gl.FLOAT, false, 0, 0);

        gl.bindBuffer(gl.ARRAY_BUFFER, starVertexPositionBuffer);
        gl.vertexAttribPointer(shaderProgram.vertexPositionAttribute, starVertexPositionBuffer.itemSize, gl.FLOAT, false, 0, 0);

        setMatrixUniforms();
        gl.drawArrays(gl.TRIANGLE_STRIP, 0, starVertexPositionBuffer.numItems);
    }



    function Star(startingDistance, rotationSpeed) {
        this.angle = 0;
        this.dist = startingDistance;
        this.rotationSpeed = rotationSpeed;

        // Set the colors to a starting value.
        this.randomiseColors();
    }

    Star.prototype.draw = function (tilt, spin, twinkle) {
        mvPushMatrix();

        // Move to the star's position
        mat4.rotate(mvMatrix, degToRad(this.angle), [0.0, 1.0, 0.0]);
        mat4.translate(mvMatrix, [this.dist, 0.0, 0.0]);

        // Rotate back so that the star is facing the viewer
        mat4.rotate(mvMatrix, degToRad(-this.angle), [0.0, 1.0, 0.0]);
        mat4.rotate(mvMatrix, degToRad(-tilt), [1.0, 0.0, 0.0]);

        if (twinkle) {
            // Draw a non-rotating star in the alternate "twinkling" color
            gl.uniform3f(shaderProgram.colorUniform, this.twinkleR, this.twinkleG, this.twinkleB);
            drawStar();
        }

        // All stars spin around the Z axis at the same rate
        mat4.rotate(mvMatrix, degToRad(spin), [0.0, 0.0, 1.0]);

        // Draw the star in its main color
        gl.uniform3f(shaderProgram.colorUniform, this.r, this.g, this.b);
        drawStar()

        mvPopMatrix();
    };


    var effectiveFPMS = 60 / 1000;
    Star.prototype.animate = function (elapsedTime) {
        this.angle += this.rotationSpeed * effectiveFPMS * elapsedTime;

        // Decrease the distance, resetting the star to the outside of
        // the spiral if it's at the center.
        this.dist -= 0.01 * effectiveFPMS * elapsedTime;
        if (this.dist < 0.0) {
            this.dist += 5.0;
            this.randomiseColors();
        }

    };


    Star.prototype.randomiseColors = function () {
        // Give the star a random color for normal
        // circumstances...
        this.r = Math.random();
        this.g = Math.random();
        this.b = Math.random();

        // When the star is twinkling, we draw it twice, once
        // in the color below (not spinning) and then once in the
        // main color defined above.
        this.twinkleR = Math.random();
        this.twinkleG = Math.random();
        this.twinkleB = Math.random();
    };



    var stars = [];

    function initWorldObjects() {
        var numStars = 50;

        for (var i=0; i < numStars; i++) {
            stars.push(new Star((i / numStars) * 5.0, i / numStars));
        }
    }


    function drawScene() {
        gl.viewport(0, 0, gl.viewportWidth, gl.viewportHeight);
        gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);

        mat4.perspective(45, gl.viewportWidth / gl.viewportHeight, 0.1, 100.0, pMatrix);

        gl.blendFunc(gl.SRC_ALPHA, gl.ONE);
        gl.enable(gl.BLEND);

        mat4.identity(mvMatrix);
        mat4.translate(mvMatrix, [0.0, 0.0, zoom]);
        mat4.rotate(mvMatrix, degToRad(tilt), [1.0, 0.0, 0.0]);

        var twinkle = document.getElementById("twinkle").checked;
        for (var i in stars) {
            stars[i].draw(tilt, spin, twinkle);
            spin += 0.1;
        }

    }


    var lastTime = 0;

    function animate() {
        var timeNow = new Date().getTime();
        if (lastTime != 0) {
            var elapsed = timeNow - lastTime;

            for (var i in stars) {
                stars[i].animate(elapsed);
            }
        }
        lastTime = timeNow;

    }


    function tick() {
        handleKeys();
        drawScene();
        animate();
        requestAnimFrame(tick);
    }



    function webGLStart() {
        var canvas = document.getElementById("lesson09-canvas");
        initGL(canvas);
        initShaders();
        initBuffers();
        initTexture();
        initWorldObjects();

        gl.clearColor(0.0, 0.0, 0.0, 1.0);

        document.onkeydown = handleKeyDown;
        document.onkeyup = handleKeyUp;

        tick();
    }
</script>
</head>
<body>
  <p><a href="lesson08.html">Previous lesson</a> - <a href="lesson10.html">Next lesson</a></p>
  <canvas width=500 height=500 id="lesson09-canvas"></canvas>
  <img src="star.gif" id="star" style="position:absolute; top: -300px; left: -300px;"/>
  <br/>
  <input type="checkbox" id="twinkle" /> Twinkle<br/>
  (Use up/down cursor keys to rotate, and <code>Page Up</code>/<code>Page Down</code> to zoom out/in)

  <p>See the original LearningWebGL.com <a href="http://learningwebgl.com/lessons/lesson09/index.html">lesson09</a>.</p> 

  <script type="text/javascript">
    window.onload = function() {
        jebgl(document.getElementById('lesson09-canvas'),webGLStart, 
              { jebglJar: 'jebgl.jar',
	        jarLocation: '/webstart/',
		jnlpLocation: 'http://jebgl.com/webstart/',
		alwaysApplet: true});
    };
  </script>
  <script type="text/javascript">
    // Modified, cross-browser requestAnimationFrame. The fallback provided by 
    // Google didn't cap the framerate at 60 Hz. It does now.
    requestAnimFrame = (function () {
        return window.requestAnimationFrame ||
            window.webkitRequestAnimationFrame ||
            window.mozRequestAnimationFrame ||
            window.oRequestAnimationFrame ||
            window.msRequestAnimationFrame ||
            function(callback, element) {
                // Set new timeout only if we've finished the previous one
                if(typeof(window.reqAnimFrameTimeout) == "undefined") window.reqAnimFrameTimeout = window.setTimeout(function() { window.reqAnimFrameTimeout = undefined; callback(); }, 1000/100);
            };
    })();
  </script>
  <script type="text/javascript">
    var _gaq = _gaq || [];
    _gaq.push(['_setAccount', 'UA-79097-21']);
    _gaq.push(['_trackPageview']);
    
    (function() {
        var ga = document.createElement('script'); ga.type = 'text/javascript'; ga.async = true;
        ga.src = ('https:' == document.location.protocol ? 'https://ssl' : 'http://www') + '.google-analytics.com/ga.js';
        var s = document.getElementsByTagName('script')[0]; s.parentNode.insertBefore(ga, s);
    })();
  </script>
</body>
</html>
